#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Author  : Leo.W
@Date    : 2024/1/30 14:27
@File    : 9-module.py
@Software: PyCharm
"""
import sys

# ---------- 模块 ---------- #
"""
在 Python 里面，一个 .py 文件可以称为模块，包含了 __init__.py 文件夹的称为包。
在模块内部，通过全局变量 __name__ 可以获取模块名（即字符串）
"""
rjust_width: int = 15  # 模块中变量，:int 为该变量的注解，表示改变了时强类型int


def rjust_print(k: str, v: any) -> None:
    print(k.rjust(rjust_width), ":", v)


# __name__ :当前模块名称，默认为文件名。当使用 python XXX.py 运行该脚本时则为 "__main__"
rjust_print("__name__", __name__)
# __package__ ：本模块所在的包名,注意：当 __name__ 为 "__main__" 时 __package__ 始终为 None。
rjust_print("__package__", __package__)
# __annotations__ :用于收集模块中变量的注解 -> {'变量名':<变量类型>,...}
rjust_print("__annotations__", __annotations__)  # {'rjust_width': <class 'int'>}
# __builtins__ : 内建模块，包含python内置类、函数、数据类型等，如：str、sum、super、tuple、type 等等
rjust_print("__builtins__", __builtins__)  # <module 'builtins' (built-in)>
# __doc__ :文档说明(只能在文件的最开始，import前面，在后面的字符串不会打印)，即文档中的字符串
rjust_print("__doc__", __doc__)
# __file__ :本文件的路径
rjust_print("__file__", __file__)
# __loader__ :由加载器在导入的模块上设置的属性，访问它时将会返回加载器对象本身。
rjust_print("__loader__", __loader__)
# __spec__ :
rjust_print("__spec__", __spec__)


# ---------- 导入该模块 import ---------- #
"""
import 模块名
模块名后使用 as 时，直接把 as 后的名称与导入模块绑定：
import fibo as fib

使用 from...import... 语句导入模块：
from fibo import *                  # 导入全部所有不以下划线（_）开头的名称,不推荐使用。这种方式向解释器导入了一批未知的名称，可能会覆盖已经定义的名称。
from fibo import fib, fib2          # 导入指定的成员
from fibo import fib as fibonacci   # 导入指定成员并给该成员起个别名

备注:
为了保证运行效率，每次解释器会话只导入一次模块。如果更改了模块内容，必须重启解释器；
仅交互测试一个模块时，也可以使用 importlib.reload()，
例如 
import importlib; 
importlib.reload(modulename)

---- PEP8导入该模块规范----：
1. 导入语句应卸载文件的顶部，但在模块 (.py 文件) 注释或说明文字之后
2. 导入语句要根据导入内容的不同进行分类，一般将其分为 3 类：
    第一类：导入 Python 内置模块
    第二类：导入相关的第三方库模块
    第三类：导入程序本地的模块 (即当前应用的模块)
3. 导入不同类别模块时，需要使用空行分开

如下示例：

# 内置模块
import os
import time
# 第三方模块
import flask
# 本地模块
from test import test1

---- import 运行流程，以 import os 为例 ----：
   (import os)
        |
[检索 sys.modules 缓存] --<存在>->[返回缓存映射的内容]->[导入]->(流程结束)
        |
     <不存在>
        |
[检索内置模块列(标准库)] --<存在>->[返回内容]->[导入]->(流程结束) ==内置模块列表是 Python 预先安装的，简单理解就是 Python 的标准库，os 就是标准库，所以 import 流程至此结束==
        |
     <不存在>
        |
[检索 sys.path 列表] --<存在>->[sys.path 列表中的路径]->[本地作用域内初始化相应的 module 对象]->(流程结束)
        |
     <不存在>
        |
[报错，抛出异常：ModuleNotFoundError: No module named 'XXX']->(流程结束)

sys.path 其实时所有Python模块所在目录的列表包，含（按一下顺序一次检索）:
  1、当前目录：被命令行直接运行的脚本所在的目录（或未指定文件时的当前目录）。
  2、PYTHONPATH 环境变量 中的每个目录。
  3、Python 默认安装路径。UNIX下，默认路径一般为/usr/local/lib/python/。

综上所述：当我们导入一个模块时： import xxx ，默认情况下python解释器会搜索【当前目录】、【已安装的内置模块】和【第三方模块】
如果找不到模块，或者动态创建了模块，需要让Python 解释器知道，可以使用 sys.path.append('file_path') 方法动态添加模块：
sys.path.append("/usr/lib/test.py")
"""


def import_test():
    print("------ sys.modules ------")
    # 获取最长key的长度->使用序列推导式获取一个每个key的长度的序列->再使用max函数去最大值
    l = max(len(k) for k in sys.modules.keys())
    for k, v in sys.modules.items():
        print(k.rjust(l), "\t", v)  # s.rjust(width) 右对齐，width表示长度

    print("------ sys.path ------")
    for p in sys.path:
        print(p)


# ---------- dir() 函数 ---------- #
"""
内置函数 dir() 用于查找模块定义的名称。返回结果是经过排序的字符串列表：
列出所有成员的名称：变量，模块，函数，……。
dir() 不会列出内置函数和变量的名称。
dir(module_name) : 获取指定模块的成员列表
dir() ：获取当前模块的成员列表
"""


def dir_test():
    print("---------- dir() ----------")


# ---------- 包 ---------- #
"""
=== 包的定义 ===
package 是一个文件夹，一个包含着一个 __init__.py 文件和若干.py文件的文件夹

注意：
1. __init__.py 中声明的变量，函数，类等等对于package来说是全局变量
2. __init__.py 的文件内容可以为空，但是这个文件必须存在
3. 一个package被导入时__init__.py 中的可执行代码会被执行，但无论包被导入多少次而__init__.py的代码都【只会被执行一次】

示例项目目录结构：
-----------------------------------------------
sound/              # op-level package
  __init__.py       # Initialize the sound package
  main.py           # 启动程序
  formats/          # Subpackage for file format conversions
      __init__.py
      wavread.py
      ...
  effects/          # Subpackage for sound effects
      __init__.py
      echo.py
      surround.py
      reverse.py
      ...
  filters/          # Subpackage for filters
      __init__.py
      karaoke.py
      ...
-----------------------------------------------

=== 导入包 or 包的单个模块 or 包中某个模块的单个成员 ===
从包中导入单个模块：
import sound.effects.echo 
# 加载子模块 sound.effects.echo。 
# 引用它必须通过其全名来引用：sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

另一种导入子模块的方法是：
from sound.effects import echo
# 引用echo即可：echo.echofilter(input, output, delay=0.7, atten=4)

直接导入所需的函数或变量：
from sound.effects.echo import echofilter

注意：
使用 from package import item 时，item 可以是包的子模块（或子包），也可以是包中定义的函数、类或变量等其他名称。
import 语句首先测试包中是否定义了 item；如果未在包中定义，则假定 item 是模块，并尝试加载。如果找不到 item，则触发 ImportError 异常。

=== 导入整个包：import package ===
使用 import sound 来导入 sound 这个 package，但是 sound 的 子包（effects,filters,formats）并不会被自动导入。
子包不会被自动导入的原因是因为：在 sound/__init__.py 文件并没有任何关于导入子package的代码。
如果需要在导入父包 sound 时Python帮你自动导入sound.effects , 可以往 sound/__init__.py 中导入子包，如下：
# sound/__init__.py
from . import effects  # 使用显式相对导入

"""

# ---------- from package import * 发生了什么？ ---------- #
"""
1.如果包的 __init__.py 代码定义了列表 __all__:
运行 from package import * 时，__all__ 就是被导入的元素名列表。可以包含：
  -  __init__.py 中定义的在本包内使用的全局变量、函数、类等
  - 本包内的模块
  
  例如：
  假设如下为 effects/__init__.py 中定义的 __all__：
    __all__ = [
        "echo",      # refers to the 'echo.py' file
        "surround",  # refers to the 'surround.py' file
        "reverse",   # !!! refers to the 'reverse' function now !!!
    ]
  from sound.effects import * 将只导入 echo 和 surround 这两个子模块，但【不会】导入 reverse 子模块，因为它被本地定义的 reverse 函数所覆盖:
  
2.如果没有定义 __all__:
  from package import * 语句【不会】把包中的所有子模块都导入到当前命名空间；
  它只是确保包 sound.effects 已被导入（可能还会运行 __init__.py 中的初始化代码），然后再导入包中定义的成员（括由 __init__.py 定义所有成员）。
  导出将按照以下规则执行：
   -> 此 package 被导入，并且执行 __init__.py 中可被执行的代码
   -> __init__.py 中定义的 variable 被导入
   -> __init__.py 中被显式导入的 module 被导入
   

"""

# ---------- 绝对导入和相对导入 ---------- #
"""
假设示例项目目录：
-----------------------------
package/                # 项目根目录,父包
    __init__.py
    moduleA.py          # 根目录下moduleA模块
    sub_package1/           # 子包 sub_package1
        __init__.py
        moduleX.py
        moduleY.py
    sub_package2/           # 子包 sub_package2
        __init__.py
        moduleZ.py
-----------------------------------------------

=== 绝对导入 ===
在上述示例中使用 import <> 或 from <> import <> 这两种语法都是绝对导入,如下示例：
    import package.sub_package1.moduleX as moduleX
    from package.sub_package1 import moduleY
    from sub_package2 import moduleZ
    
-> 在 Python3.x 中，绝对导入是默认的导入形式，也是 PEP 8 推荐的导入形式，它相当直观，可以很明确的知道要导入的包或模块在哪里
-> 需要注意：使用绝对导入时在【当前模块】中引入【同级别的模块】或者【同级别的包下的其他模块】的时候，最好把【导入路径写完整】。

以示例项目为例，假设在 package/sub_package1/moduleX.py 中需要导入同目录下得 moduleY.py：
# moduleX.py 中：
import moduleY

# moduleA.py 中：
import package.sub_package1.moduleX

运行 main.py 将会报错:
-> ModuleNotFoundError: No module named 'moduleY'
-> 因为执行模块 moduleA.py 的路径是D:/package/（本机的目录）， moduleX.py 中导入语句 import moduleY 将会被解析并定位到 D:/package/moduleY.py，很明显这个路径不存在。
-> 由此可见绝对导入时，解释器会根据当前执行的.py文件所在目录去寻找文件中导入的模块（如果未指定包）。
-> 解决上面问题需要模块  moduleX.py 中 import moduleY.py 语句路径补充完整：import package.sub_package1.moduleY 或者使用【相对导入】 from . import moduleY

=== 相对导入 ===
相对导入是指 同一个【顶层 package】内部不同 module 之间的导入行为：
相对导入只能使用 from <> import <> 这种语法，并且使用 . 作为前导点
-> .    表示当前目录
-> ..   表示上层目录
-> ...  表示上上层目录
-> .... 以此类推

相对导入解决了绝对导入的一些问题：
-> 同一个包下的模块可以很方便的相互引用，使用像 from . import xxx 的语句就行。
-> 顶层包的报名改了，包下的模块的相对导入的语句基本不用改。

import moduleY            # 隐式相对导入,隐式相对导入容易与绝对导入混淆，非常不推荐，已被 Python3 废弃。
from . import moduleY     # 显式相对导入

=== 绝对导入与相对导入对比 ===
1. 绝对导入由于其含义非常明确，且在任何调起方式中均可以使用，因而被 PEP8 所推荐。
2. 绝对导入唯一的缺点是将 package 名称硬编码到了代码中，会带来维护问题。例如修改了某一顶层包名之后，那么其内部的所有绝对导入代码都需要相应修改。
3. 而相对导入就可以避免这种维护问题，当包名修改之后内部代码无需做任何改动。
   但相对导入的解析机制更加复杂，容易因为使用不当而报错。并且使用了相对导入的 py 文件无法再作为脚本直接运行。

=== 最佳实践 ===
1. 一般情况下使用绝对导入。
2. 如果要构建一个 package 供外部调用，例如给其他脚本调用或发布到 PYPI，则在该 package 内部使用相对导入。
3. 对于使用了相对导入的脚本，如果想直接运行其中的 if __name__ == '__main__': 代码块（通常用于简单测试当前 module 的功能），
   可以使用 python -m package.module 的方式调起，避免使用 python package/module.py。
"""


# ---------- __all__ ---------- #
"""
__all__ : 用于显式声明 from module import * 或from package import * 中的 * 代表的成员。(可以先看完module和package的定义后回头看这里)
 + 对于module来说，如果__all__未定义则 * 导入全部的标识符
 + 对于package来说，如果__all__未定义则 * 不导入所有的标识符

__all__在module(模块)中时：
因为模块是单一的文件，所以__all__就被定义在该文件（模块）内.
例如:

# test.py
a = 'a string'
b = 3.14
def c():
    pass

__all__ = ['a', 'c']

在同目录下的另一个文件中导入:

from test import *
print(b)       # 将导致错误

__all__在package(包)中时:
在package这种场景下，在 __init__.py 文件中定义 __all__，管理 from package import * 时能导入的 module.

"""
# 指定当在其他模块中使用 from module import * 语句导入该模块时，可以导入的该模块的成员列表
__all__ = ["import_test", "dir_test"]

# ---------- Python 程序的三种调起方式 ---------- #
"""
方式 1：作为脚本直接运行：python package/sub_package1/moduleX.py
方式 2：作为模块直接运行：python -m package.sub_package1.moduleX
方式 3：从别的模块中导入：import package.sub_package1.moduleX

=== Python 的导入机制依赖 sys.path、__package__ 和 __name__ 三个变量 === 
以上三种调用方式会对这三个变量产生不同的作用，如下：
方式 1：__package__ 为 None; __name__ 为 '__main__'; 【当前目录(package/)】和【脚本所在目录(package/sub_package1/)】被加入 sys.path。
方式 2：__package__ 为 'package'; __name__ 为 '__main__'; 当前目录被加入 sys.path。
方式 3：__package__ 为 'package'; __name__ 为 'moduleA'。sys.path 中具体加入了什么路径，要看程序入口是怎么调起的。

=== 所谓相对导入，相对的就是 __package__ 所代表的包名 ===
当执行 from .moduleY import func 时，实际上相当于解析 from __package__.moduleY import func:
如果 __package__ 为 None，则会解析 from __name__.moduleY import func

=== 相对导入可能抛出的错误包括以下几种(由 __package__ 的取值逻辑导致)：===
 -> 如果 __package__ 为 ''（一般出现在类似 python -m moduleX 这样的调用方式中），
    会抛出 ImportError: attempted relative import with no known parent package 错误。
 -> 如果 __package__ 不为空，但在 sys.path 的所有路径中均未搜索到 __package__ 所代表的包名，
    会抛出类似 ModuleNotFoundError: No module named 'xxxpackage.moduleY'; 'xxxpackage' is not a package 的错误。
 -> 如果 __package__ 不为空且存在对应的包，但其中没有 moduleY 模块，
    会抛出类似 ModuleNotFoundError: No module named 'xxxpackage.moduleY' 的错误。
 -> 如果试图从上级 package 中进行相对导入，例如 from ..moduleA import func，那么必须确保 __package__ 是多级 package，
    例如 __package__ = 'package.subpackage1'。如果 package 级别数小于上溯的级别数，
    例如 __package__ = 'subpackage1'，将会抛出 ValueError: attempted relative import beyond top-level package 错误。
   
由此可见:
相对导入必须确保 __package__ 有合适的取值，也就是只能用于上述[方式  2、3]调用方式。
尽管[方式 1]调起是最常用的，但不幸的是在这种方式下只能使用绝对导入，不能使用相对导入。
-------------------------------------------
python moduleX.py <arguments>
执行上面代码，和导入模块一样，但会把 __name__ 赋值为 "__main__"
将下面代码加入到文件末尾，将产生如下效果：
 - 当python文件以【脚本】形式直接被运行时，if __name__ == '__main__': 语句下面的代码段将被执行。
 - 当python文件以【模块】形式被 import 时，if __name__ == '__main__': 语句下面的代码段【不会】被执行。
"""
if __name__ == "__main__":
    import_test()
    dir_test()

    print("---------- 当前模块的成员 ----------")
    modules = dir()  # 没有参数时，dir() 列出当前已定义的名称
    ml = max(len(x) for x in modules)
    for m in modules:  # 遍历当前模块成员
        try:
            # 将字符串转换成代码（使用eval()函数）
            print(m.rjust(ml), "\t", type(eval(m)))  # 这里的time替换成需要查询的对象
        except Exception as err:
            print(err)
